using System;
using System.Collections.Generic;
using System.Text;
//using Camera;
using DS.SludgeEngine.Base;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using DS.SludgeEngine.Base.GlobalContent;

namespace DS.SludgeEngine.Entities
{
    public class Actor : IActor
    {
        #region Private Members
        //Location and orientation
        private float _yaw;
        private float _pitch;
        private float _roll;
        private Quaternion _rotation;
        private Vector3 _position;
        private Vector3 _destination;
        private float _height;
        private EntityController parent;
        //State
        private ActorState _state;
        private bool _isMouseOver;
        private bool _isSelected;
        private bool _isDragged;
        private bool _dead = false;

        //Unit specific
        protected Model _model;
        private BoundingSphere _mySphere;
        ActorInfo info = new ActorInfo();
        private int currentHealth;

        //Behaviour
        IActor target = null;

        #endregion

        #region Public Properties
        public float Yaw
        {
            get { return _yaw; }
            set { _yaw = value; }
        }

        public float Pitch
        {
            get { return _pitch; }
            set { _pitch = value; }
        }

        public float Roll
        {
            get { return _roll; }
            set { _roll = value; }
        }

        public Model UnitModel
        {
            get { return _model; }
            set { _model = value; }
        }

        public Quaternion ForwardView
        {
            get
            {
                ITerrainComponent terrain = (ITerrainComponent)GameInformation.Game.Services.GetService(typeof(ITerrainComponent));
                Quaternion inclination = RotateNormal(terrain.GetNormal((int)_position.X, (int)_position.Z));
                return inclination * _rotation;
            }
        }

        public Quaternion Rotation
        {
            get { return _rotation; }
            set { _rotation = value; }
        }

        public Vector3 Position
        {
            get { return _position; }
            set { _position = value; }
        }
        public Vector3 AdjustedPosition
        {
            get { return new Vector3(_position.X, _height, _position.Z); }

        }
        public Vector3 Destination
        {
            get { return _destination; }
            set { _destination = value; }
        }
        public ActorState State
        {
            get { return _state; }
            set { _state = value; }
        }
        public bool IsMouseOver
        {
            get { return _isMouseOver; }
            set { _isMouseOver = value; }
        }

        public bool IsSelected
        {
            get { return _isSelected; }
            set { _isSelected = value; }
        }

        public bool IsDragged
        {
            get { return _isDragged; }
            set { _isDragged = value; }
        }
        public float Height
        {
            get { return _height; }
            set { _height = value; }
        }
        public ActorInfo Info
        {
            get { return info; }
            set { info = value; }
        }
        public BoundingSphere Sphere
        {
            get { return _mySphere; }
            set { _mySphere = value; }
        }

        public IActor Target
        {
            get { return target; }
            set { target = value; }
        }


        public int CurrentHealth
        {
            get { return currentHealth; }
            set { currentHealth = value; }
        }
        #endregion

        public Actor(Game game, ActorInfo info, EntityController controller)
        {
            _state = ActorState.Rest;
            parent = controller;
            Info = info;
            // Load the tank model from the ContentManager.
            _model = GlobalContent.Models[Info.Model];
            if (Info.RotationDenominator != 0)
            {
                Info.RotationOffset = (Info.RotationNumerator * (float)Math.PI) / Info.RotationDenominator;
            }
            currentHealth = info.MaxHealth;
        }

        public void Hit(float damage)
        {
            currentHealth -= (int)damage;
            if (currentHealth <= 0 && _dead != true)
            {
                _dead = true;
                parent.UnitDestroyed(this);
            }
        }

        public void SetDestination(Vector3 destination)
        {
            _state = ActorState.Rotating;
            _destination = destination;
        }
        public void SetAttack(IActor highlightedActor)
        {
            target = highlightedActor;
            _state = ActorState.AttackRotating;
        }

        public bool Update(GameTime gameTime, GraphicsDevice device, ICameraComponent camera, ITerrainComponent terrain, IDrawing2DManager drawing, IProjectileController projectileController)
        {
            if (target != null && target.CurrentHealth <= 0)
            {
                target = null;
                _state = ActorState.Rest;
            }
            CheckMouseOver(device, camera);
            DrawHealth(drawing, device, camera);
            switch (_state)
            {
                case ActorState.Rest:
                    break;
                case ActorState.Rotating:
                    if (Rotate(_destination, 0.01f))
                    {
                        _state = ActorState.Moving;
                    }
                    break;
                case ActorState.Moving:
                    Rotate(_destination, 0.0f);
                    if (Move(_destination, 0.1f))
                    {
                        _state = ActorState.Rest;
                    }
                    break;
                case ActorState.AttackRotating:
                    if (Rotate(target.Position, 1.0f))
                    {
                        _state = ActorState.AttackMoving;
                    }
                    break;
                case ActorState.AttackMoving:
                    Rotate(target.Position, 0.0f);
                    if (Move(target.Position, info.Range / 2))
                    {
                        _state = ActorState.AttackInRange;
                    }
                    break;
                case ActorState.AttackInRange:
                    Rotate(target.Position, 0.0f);
                    InRange(gameTime, projectileController);
                    break;
                default:
                    break;
            }
            UpdateSpecific(gameTime);
            return _isMouseOver;
        }
        long lastFired = 0;
        private void InRange(GameTime gameTime, IProjectileController projectileController)
        {
            if (CalculateTolerance(_position, target.Position, info.Range) == false)
            {
                _state = ActorState.AttackRotating;
            }


            if ((DateTime.Now.Ticks - lastFired) >= info.RefireRate)
            {
                lastFired = DateTime.Now.Ticks;
                //Vector3 projectileDirection = Vector3.Transform(new Vector3(0, 0, -1), _rotation);

                Vector3 projectileDirection = target.AdjustedPosition - AdjustedPosition;
                projectileDirection.Normalize();

                if (info.Name == "Ship")
                {
                    projectileController.Add(projectileDirection, new Vector3(_position.X, _height + info.ModelOffset, _position.Z), 0.2f, 100000000, target, false);
                }
                else
                {
                    projectileController.Add(projectileDirection, new Vector3(_position.X, _height + info.ModelOffset, _position.Z), 0.2f, 100000000, target, true);
                }
            }
        }

        public void Draw(bool reflecting, GraphicsDevice device, ICameraComponent camera, ITerrainComponent terrain, IDrawing2DManager drawing)
        {
            Matrix[] transforms = new Matrix[_model.Bones.Count];
            _model.CopyAbsoluteBoneTransformsTo(transforms);

            GetHeight(terrain);

            Matrix modelWorld = GetWorldMatrix(terrain);

            Matrix view = GetViewMatrix(reflecting, camera, terrain);

            Draw(modelWorld, view, camera.ProjectionMatrix);
            LocateSphere();



        }
        #region Draw Methods
        private void DrawHealth(IDrawing2DManager drawing, GraphicsDevice device, ICameraComponent camera)
        {
            if (_isSelected || _isMouseOver || _isDragged)
            {
                Vector3 location = device.Viewport.Project(AdjustedPosition,
                    camera.ProjectionMatrix,
                    camera.ViewMatrix,
                    Matrix.Identity);

                float healthPercentage = (float)currentHealth / (float)info.MaxHealth;
                float health = ((float)30 * healthPercentage) - 15;

                Color healthColor = new Color(new Vector3(1.0f - (1.0f * healthPercentage), 1.0f * healthPercentage, 0));
                drawing.AddRectangle(new Vector2(location.X - 16, location.Y - 1), new Vector2(location.X + 16, location.Y + 2), Color.White, false);
                drawing.AddRectangle(new Vector2(location.X - 15, location.Y), new Vector2(location.X + health, location.Y + 1), healthColor, false);

            }
        }

        private void LocateSphere()
        {
            _mySphere.Center = _position;
            _mySphere.Center.Y = _height + info.SphereOffset;
            _mySphere.Radius *= info.Scale;
        }

        private void CheckMouseOver(GraphicsDevice device, ICameraComponent camera)
        {
            Ray cursorRay = CalculateCursorRay(camera.ProjectionMatrix
                , camera.ViewMatrix, device);
            if (_mySphere.Intersects(cursorRay) != null)
            {
                _isMouseOver = true;
            }
            else
            {
                _isMouseOver = false;
            }
        }

        private static Matrix GetViewMatrix(bool reflecting, ICameraComponent camera, ITerrainComponent terrain)
        {
            Matrix view = Matrix.Identity;
            if (reflecting)
            {
                view = terrain.ReflectionViewMatrix;
            }
            else
            {
                view = camera.ViewMatrix;
            }
            return view;
        }

        private Matrix GetWorldMatrix(ITerrainComponent terrain)
        {
            Quaternion inclination = RotateNormal(terrain.GetNormal((int)_position.X, (int)_position.Z));
            Matrix modelWorld = Matrix.CreateScale(info.Scale);
            modelWorld *= Matrix.CreateFromQuaternion(_rotation);
            modelWorld *= Matrix.CreateRotationY(info.RotationOffset);
            modelWorld *= Matrix.CreateFromQuaternion(inclination);
            modelWorld *= Matrix.CreateTranslation(_position + new Vector3(0.0f, _height + info.ModelOffset, 0.0f));
            return modelWorld;
        }

        private void GetHeight(ITerrainComponent terrain)
        {
            _height = terrain.GetTerrainHeight(_position.X, _position.Z);
            if (_height < terrain.MinHeight)
            {
                _height = terrain.MinHeight;
            }
        }
        #endregion
        #region Virtual Methods
        public virtual void Draw(Matrix world, Matrix view, Matrix projection)
        {
            throw new NotImplementedException("You must override the draw method in the inheriting class");
        }

        public virtual void UpdateSpecific(GameTime gameTime)
        {
            throw new NotImplementedException("You must override the UpdateSpecific method in the inheriting class");
        }
        #endregion
        #region Picking
        // CalculateCursorRay Calculates a world space ray starting at the camera's
        // "eye" and pointing in the direction of the cursor. Viewport.Unproject is used
        // to accomplish this. see the accompanying documentation for more explanation
        // of the math behind this function.
        public Ray CalculateCursorRay(Matrix projectionMatrix, Matrix viewMatrix, GraphicsDevice device)
        {
            // create 2 positions in screenspace using the cursor position. 0 is as
            // close as possible to the camera, 1 is as far away as possible.
            Vector2 position = new Vector2();
            MouseState mouseState = Mouse.GetState();
            position.X = mouseState.X;
            position.Y = mouseState.Y;

            Vector3 nearSource = new Vector3(position, 10f);
            Vector3 farSource = new Vector3(position, 15f);

            // use Viewport.Unproject to tell what those two screen space positions
            // would be in world space. we'll need the projection matrix and view
            // matrix, which we have saved as member variables. We also need a world
            // matrix, which can just be identity.
            Vector3 nearPoint = device.Viewport.Unproject(nearSource,
                projectionMatrix, viewMatrix, Matrix.Identity);

            Vector3 farPoint = device.Viewport.Unproject(farSource,
                projectionMatrix, viewMatrix, Matrix.Identity);

            // find the direction vector that goes from the nearPoint to the farPoint
            // and normalize it....
            Vector3 direction = farPoint - nearPoint;
            direction.Normalize();

            // and then create a new ray using nearPoint as the source.
            return new Ray(nearPoint, direction);
        }

        /*
        /// <summary>
        /// This helper function checks to see if a ray will intersect with a model.
        /// The model's bounding spheres are used, and the model is transformed using
        /// the matrix specified in the worldTransform argument.
        /// </summary>
        /// <param name="ray">the ray to perform the intersection check with</param>
        /// <param name="model">the model to perform the intersection check with.
        /// the model's bounding spheres will be used.</param>
        /// <param name="worldTransform">a matrix that positions the model
        /// in world space</param>
        /// <param name="absoluteBoneTransforms">this array of matrices contains the
        /// absolute bone transforms for the model. this can be obtained using the
        /// Model.CopyAbsoluteBoneTransformsTo function.</param>
        /// <returns>true if the ray intersects the model.</returns>
        private static bool RayIntersectsModel(Ray ray, Model model,
            Matrix worldTransform, Matrix[] absoluteBoneTransforms)
        {
            // Each ModelMesh in a Model has a bounding sphere, so to check for an
            // intersection in the Model, we have to check every mesh.
            foreach (ModelMesh mesh in model.Meshes)
            {
                // the mesh's BoundingSphere is stored relative to the mesh itself.
                // (Mesh space). We want to get this BoundingSphere in terms of world
                // coordinates. To do this, we calculate a matrix that will transform
                // from coordinates from mesh space into world space....
                Matrix world =
                    absoluteBoneTransforms[mesh.ParentBone.Index] * worldTransform;

                // ... and then transform the BoundingSphere using that matrix.
                BoundingSphere sphere =
                    TransformBoundingSphere(mesh.BoundingSphere, world);

                // now that the we have a sphere in world coordinates, we can just use
                // the BoundingSphere class's Intersects function. Intersects returns a
                // nullable float (float?). This value is the distance at which the ray
                // intersects the BoundingSphere, or null if there is no intersection.
                // so, if the value is not null, we have a collision.
                if (sphere.Intersects(ray) != null)
                {
                    return true;
                }
            }

            // if we've gotten this far, we've made it through every BoundingSphere, and
            // none of them intersected the ray. This means that there was no collision,
            // and we should return false.
            return false;
        }

        /// <summary>
        /// This helper function takes a BoundingSphere and a transform matrix, and
        /// returns a transformed version of that BoundingSphere.
        /// </summary>
        /// <param name="sphere">the BoundingSphere to transform</param>
        /// <param name="world">how to transform the BoundingSphere.</param>
        /// <returns>the transformed BoundingSphere/</returns>
        private static BoundingSphere TransformBoundingSphere(BoundingSphere sphere,
            Matrix transform)
        {
            BoundingSphere transformedSphere;

            // the transform can contain different scales on the x, y, and z components.
            // this has the effect of stretching and squishing our bounding sphere along
            // different axes. Obviously, this is no good: a bounding sphere has to be a
            // SPHERE. so, the transformed sphere's radius must be the maximum of the 
            // scaled x, y, and z radii.

            // to calculate how the transform matrix will affect the x, y, and z
            // components of the sphere, we'll create a vector3 with x y and z equal
            // to the sphere's radius...
            Vector3 scale3 = new Vector3(sphere.Radius, sphere.Radius, sphere.Radius);

            // then transform that vector using the transform matrix. we use
            // TransformNormal because we don't want to take translation into account.
            scale3 = Vector3.TransformNormal(scale3, transform);

            // scale3 contains the x, y, and z radii of a squished and stretched sphere.
            // we'll set the finished sphere's radius to the maximum of the x y and z
            // radii, creating a sphere that is large enough to contain the original 
            // squished sphere.
            transformedSphere.Radius = Math.Max(scale3.X, Math.Max(scale3.Y, scale3.Z));

            // transforming the center of the sphere is much easier. we can just use 
            // Vector3.Transform to transform the center vector. notice that we're using
            // Transform instead of TransformNormal because in this case we DO want to 
            // take translation into account.
            transformedSphere.Center = Vector3.Transform(sphere.Center, transform);


            return transformedSphere;
        }
         * */
        #endregion
        #region Movement


        public Quaternion RotateNormal(Vector3 newNormal)
        {
            if (newNormal == new Vector3(0, 0, 0))
            {
                newNormal = new Vector3(0, 1, 0);
            }
            //else
            //{
            //    //newNormal = new Vector3(newNormal.Y, newNormal.Z, newNormal.X);
            //}
            // Obtain model "normal" from current rotation quaternion (or rotation matrix)
            Vector3 currentNormal = Matrix.CreateFromQuaternion(_rotation).Up;
            // Obtain axis between two normals, it will be the rotation axis
            Vector3 axis = Vector3.Normalize(Vector3.Cross(currentNormal, newNormal));
            // Calc angle between normals
            float angle = (float)Math.Acos(Vector3.Dot(currentNormal, newNormal));
            Quaternion m_Inclination;
            if (angle != 0.0f)
            {
                // Create a quaternion with the rotation difference between normals
                m_Inclination = Quaternion.CreateFromAxisAngle(axis, angle);
            }
            else
            {
                m_Inclination = Quaternion.Identity;
            }
            return m_Inclination;
        }
        public void Rotate(float xRotation, float yRotation, float zRotation)
        {
            this._yaw += xRotation;
            this._pitch += yRotation;
            this._roll += zRotation;

            Quaternion q1 = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), this._yaw);
            Quaternion q2 = Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), this._pitch);
            Quaternion q3 = Quaternion.CreateFromAxisAngle(new Vector3(0, 0, 1), this._roll);

            this._rotation = q1 * q2 * q3;
        }


        public void UpdateRotation()
        {
            Quaternion q1 = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), this._yaw);
            Quaternion q2 = Quaternion.CreateFromAxisAngle(new Vector3(1, 0, 0), this._pitch);
            Quaternion q3 = Quaternion.CreateFromAxisAngle(new Vector3(0, 0, 1), this._roll);

            this._rotation = q1 * q2 * q3;
        }

        public void Translate(Vector3 distance)
        {
            Vector3 diff = Vector3.Transform(distance, Matrix.CreateFromQuaternion(this._rotation));
            this._position += diff;
        }

        private bool Rotate(Vector3 rotateTo, float tolerance)
        {
            float x, y;
            bool complete = false;
            x = -_position.Z;
            y = -_position.X;
            float newRotation = TurnToFace(new Vector2(x, y)
               , new Vector2(-rotateTo.Z, -rotateTo.X), _yaw, info.RotationSpeed);
            if (Math.Abs(newRotation - _yaw) <= tolerance)
            {
                complete = true;

            }
            _yaw = newRotation;
            UpdateRotation();
            return complete;
        }

        private static float TurnToFace(Vector2 position, Vector2 faceThis, float currentAngle, float turnSpeed)
        {
            // consider this diagram:
            //         C 
            //        /|
            //      /  |
            //    /    | y
            //  / o    |
            // S--------
            //     x
            // 
            // where S is the position of the spot light, C is the position of the cat,
            // and "o" is the angle that the spot light should be facing in order to 
            // point at the cat. we need to know what o is. using trig, we know that
            //      tan(theta)       = opposite / adjacent
            //      tan(o)           = y / x
            // if we take the arctan of both sides of this equation...
            //      arctan( tan(o) ) = arctan( y / x )
            //      o                = arctan( y / x )
            // so, we can use x and y to find o, our "desiredAngle."
            // x and y are just the differences in position between the two objects.
            float x = faceThis.X - position.X;
            float y = faceThis.Y - position.Y;

            // we'll use the Atan2 function. Atan will calculates the arc tangent of 
            // y / x for us, and has the added benefit that it will use the signs of x
            // and y to determine what cartesian quadrant to put the result in.
            // http://msdn2.microsoft.com/en-us/library/system.math.atan2.aspx
            float desiredAngle = (float)Math.Atan2(y, x);

            // so now we know where we WANT to be facing, and where we ARE facing...
            // if we weren't constrained by turnSpeed, this would be easy: we'd just 
            // return desiredAngle.
            // instead, we have to calculate how much we WANT to turn, and then make
            // sure that's not more than turnSpeed.

            // first, figure out how much we want to turn, using WrapAngle to get our
            // result from -Pi to Pi ( -180 degrees to 180 degrees )
            float difference = WrapAngle(desiredAngle - currentAngle);

            // clamp that between -turnSpeed and turnSpeed.
            difference = MathHelper.Clamp(difference, -turnSpeed, turnSpeed);

            // so, the closest we can get to our target is currentAngle + difference.
            // return that, using WrapAngle again.
            return WrapAngle(currentAngle + difference);
        }

        /// <summary>
        /// Returns the angle expressed in radians between -Pi and Pi.
        /// </summary>
        private static float WrapAngle(float radians)
        {
            while (radians < -MathHelper.Pi)
            {
                radians += MathHelper.TwoPi;
            }
            while (radians > MathHelper.Pi)
            {
                radians -= MathHelper.TwoPi;
            }
            return radians;
        }

        private bool Move(Vector3 destination, float tolerance)
        {
            bool complete = false;
            if (CalculateTolerance(_position, destination, tolerance))
            {
                complete = true;
            }
            Translate(new Vector3(0.0f, 0.0f, info.TravelSpeed));
            return complete;
        }

        public bool CalculateTolerance(Vector3 current, Vector3 destination, float tolerance)
        {
            Vector3 difference = destination - current;
            if (difference.Length() <= tolerance)
            {
                return true;
            }
            return false;
        }
        #endregion

    }
}
